package leetcode.editor.cn;
//请你为 最不经常使用（LFU）缓存算法设计并实现数据结构。 
//
// 实现 LFUCache 类： 
//
// 
// LFUCache(int capacity) - 用数据结构的容量 capacity 初始化对象 
// int get(int key) - 如果键 key 存在于缓存中，则获取键的值，否则返回 -1 。 
// void put(int key, int value) - 如果键 key 已存在，则变更其值；如果键不存在，请插入键值对。当缓存达到其容量 capac
//ity 时，则应该在插入新项之前，移除最不经常使用的项。在此问题中，当存在平局（即两个或更多个键具有相同使用频率）时，应该去除 最近最久未使用 的键。 
// 
//
// 为了确定最不常使用的键，可以为缓存中的每个键维护一个 使用计数器 。使用计数最小的键是最久未使用的键。 
//
// 当一个键首次插入到缓存中时，它的使用计数器被设置为 1 (由于 put 操作)。对缓存中的键执行 get 或 put 操作，使用计数器的值将会递增。 
//
// 函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。 
//
// 
//
// 示例： 
//
// 
//输入：
//["LFUCache", "put", "put", "get", "put", "get", "get", "put", "get", "get", "g
//et"]
//[[2], [1, 1], [2, 2], [1], [3, 3], [2], [3], [4, 4], [1], [3], [4]]
//输出：
//[null, null, null, 1, null, -1, 3, null, -1, 3, 4]
//
//解释：
//// cnt(x) = 键 x 的使用计数
//// cache=[] 将显示最后一次使用的顺序（最左边的元素是最近的）
//LFUCache lfu = new LFUCache(2);
//lfu.put(1, 1);   // cache=[1,_], cnt(1)=1
//lfu.put(2, 2);   // cache=[2,1], cnt(2)=1, cnt(1)=1
//lfu.get(1);      // 返回 1
//                 // cache=[1,2], cnt(2)=1, cnt(1)=2
//lfu.put(3, 3);   // 去除键 2 ，因为 cnt(2)=1 ，使用计数最小
//                 // cache=[3,1], cnt(3)=1, cnt(1)=2
//lfu.get(2);      // 返回 -1（未找到）
//lfu.get(3);      // 返回 3
//                 // cache=[3,1], cnt(3)=2, cnt(1)=2
//lfu.put(4, 4);   // 去除键 1 ，1 和 3 的 cnt 相同，但 1 最久未使用
//                 // cache=[4,3], cnt(4)=1, cnt(3)=2
//lfu.get(1);      // 返回 -1（未找到）
//lfu.get(3);      // 返回 3
//                 // cache=[3,4], cnt(4)=1, cnt(3)=3
//lfu.get(4);      // 返回 4
//                 // cache=[3,4], cnt(4)=2, cnt(3)=3 
//
// 
//
// 提示： 
//
// 
// 0 <= capacity <= 104 
// 0 <= key <= 105 
// 0 <= value <= 109 
// 最多调用 2 * 105 次 get 和 put 方法 
// 
// Related Topics 设计 哈希表 链表 双向链表 
// 👍 643 👎 0

import java.util.HashMap;
import java.util.LinkedHashSet;

/**
 * LFU 算法是淘汰访问频次最低的数据，如果访问频次最低的数据有多条，需要淘汰最旧的数据。
 * <p>
 * // 构造一个容量为 2 的 LFU 缓存
 * LFUCache cache = new LFUCache(2);
 * <p>
 * // 插入两对 (key, val)，对应的 freq 为 1
 * cache.put(1, 10);
 * cache.put(2, 20);
 * <p>
 * // 查询 key 为 1 对应的 val
 * // 返回 10，同时键 1 对应的 freq 变为 2
 * cache.get(1);
 * <p>
 * // 容量已满，淘汰 freq 最小的键 2
 * // 插入键值对 (3, 30)，对应的 freq 为 1
 * cache.put(3, 30);
 * <p>
 * // 键 2 已经被淘汰删除，返回 -1
 * cache.get(2);
 * <p>
 * <p>
 * <p>
 * 先列举出算法执行过程中的几个显而易见的事实：
 * <p>
 * 1、调用get(key)方法时，要返回该key对应的val。
 * <p>
 * 2、只要用get或者put方法访问一次某个key，该key的freq就要加一。
 * <p>
 * 3、如果在容量满了的时候进行插入，则需要将freq最小的key删除，如果最小的freq对应多个key，则删除其中最旧的那一个。
 * <p>
 * 好的，我们希望能够在 O(1) 的时间内解决这些需求，可以使用基本数据结构来逐个击破：
 * <p>
 * 1、使用一个HashMap存储key到val的映射，就可以快速计算get(key)。
 * <p>
 * HashMap<Integer, Integer> keyToVal;
 * 2、使用一个HashMap存储key到freq的映射，就可以快速操作key对应的freq。
 * <p>
 * HashMap<Integer, Integer> keyToFreq;
 * 3、这个需求应该是 LFU 算法的核心，所以我们分开说。
 * <p>
 * 3.1、首先，肯定是需要freq到key的映射，用来找到freq最小的key。
 * <p>
 * 3.2、将freq最小的key删除，那你就得快速得到当前所有key最小的freq是多少。
 * 想要时间复杂度 O(1) 的话，肯定不能遍历一遍去找，那就用一个变量minFreq来记录当前最小的freq吧。
 * <p>
 * 3.3、可能有多个key拥有相同的freq，所以 freq对key是一对多的关系，即一个freq对应一个key的列表。
 * <p>
 * 3.4、希望freq对应的key的列表是存在时序的，便于快速查找并删除最旧的key。
 * <p>
 * 3.5、希望能够快速删除key列表中的任何一个key，因为如果频次为freq的某个key被访问，
 * 那么它的频次就会变成freq+1，就应该从freq对应的key列表中删除，加到freq+1对应的key的列表中。
 * <p>
 * HashMap<Integer, LinkedHashSet<Integer>> freqToKeys;
 * int minFreq = 0;
 * 介绍一下这个LinkedHashSet，它满足我们 3.3，3.4，3.5 这几个要求。
 * 你会发现普通的链表LinkedList能够满足 3.3，3.4 这两个要求，但是由于普通链表不能快速访问链表中的某一个节点，
 * 所以无法满足 3.5 的要求。
 * <p>
 * LinkedHashSet顾名思义，是链表和哈希集合的结合体。链表不能快速访问链表节点，
 * 但是插入元素具有时序；哈希集合中的元素无序，但是可以对元素进行快速的访问和删除。
 * <p>
 * 那么，它俩结合起来就兼具了哈希集合和链表的特性，既可以在 O(1) 时间内访问或删除其中的元素，
 * 又可以保持插入的时序，高效实现 3.5 这个需求。
 */
//leetcode submit region begin(Prohibit modification and deletion)
class LFUCache {

    // key 到 val 的映射，我们后文称为 KV 表
    HashMap<Integer, Integer> keyToVal;
    // key 到 freq 的映射，我们后文称为 KF 表
    HashMap<Integer, Integer> keyToFreq;
    // freq 到 key 列表的映射，我们后文称为 FK 表
    HashMap<Integer, LinkedHashSet<Integer>> freqToKeys;
    // 记录最小的频次
    int minFreq;
    // 记录 LFU 缓存的最大容量
    int cap;

    public LFUCache(int capacity) {
        keyToVal = new HashMap<>();
        keyToFreq = new HashMap<>();
        freqToKeys = new HashMap<>();
        this.cap = capacity;
        this.minFreq = 0;
    }

    public int get(int key) {
        if (keyToVal.containsKey(key)) {
            // 获取一次，添加一次频率freq
            increaseFreq(key);
            return keyToVal.get(key);
        }
        return -1;
    }

    public void put(int key, int value) {
        // 容量不符合规范
        if (this.cap <= 0) return;

        // 已存在改key，只需更改内容和频率
        if (keyToVal.containsKey(key)) {
            increaseFreq(key);
            keyToVal.put(key, value);
            return;
        }

        // 当容量都达到最大，删除频率最小的key且最旧的key
        if (keyToVal.size() == cap) {
            removeMinFreqKey();
        }

        // 新增keyToFreq
        keyToFreq.put(key, 1);
        // 新增keyToVal
        keyToVal.put(key, value);
        // 新增freqToKeys
        freqToKeys.putIfAbsent(1, new LinkedHashSet<>());
        freqToKeys.get(1).add(key);
        // 更新minFreq
        this.minFreq = 1;
    }

    private void removeMinFreqKey() {
        // 获取最小频率的keys
        LinkedHashSet<Integer> keys = freqToKeys.get(this.minFreq);
        // 获取最旧的key
        Integer first = keys.iterator().next();
        // 最小频率中只有一个key，直接移除freqToKeys中的一个元素，反之移除对应keys中最旧的key
        if (keys.size() == 1) {
            freqToKeys.remove(this.minFreq);
            // 问：这里需要更新 minFreq 的值吗？
            // 答：不需要，查看put代码，可以发现，调用removeMinFreqKey方法时只有put一个新的key进来，当进入之后，
            // put会将最新的key的freq更新为1，同样的minnFreq也会更新为1
        } else {
            keys.remove(first);
        }
        // 移除key之后同时移除对应的keyToVal，keyToFreq
        keyToVal.remove(first);
        keyToFreq.remove(first);
    }

    private void increaseFreq(int key) {
        // 获取当前key的freq
        Integer freq = keyToFreq.get(key);
        // 更新当前key的freq+1
        keyToFreq.put(key, freq + 1);

        // 将当前的key放入到freqToKeys，freq+1对应的LinkedHashSet中
        freqToKeys.putIfAbsent(freq + 1, new LinkedHashSet<>());
        freqToKeys.get(freq + 1).add(key);

        // 如果 freq 对应的列表空了，移除这个 freq
        // 获取旧的freq对应的LinkedHashSet，如果大小为一，移除这个 freq，反之移除对应keys中的key
        LinkedHashSet<Integer> keys = freqToKeys.get(freq);
        if (keys.size() == 1) {
            freqToKeys.remove(freq);
            // 如果这个 freq 恰好是 minFreq，更新 minFreq
            if (freq == this.minFreq) {
                this.minFreq++;
            }
        } else {
            keys.remove(key);
        }
    }
}

/**
 * Your LFUCache object will be instantiated and called as such:
 * LFUCache obj = new LFUCache(capacity);
 * int param_1 = obj.get(key);
 * obj.put(key,value);
 */
//leetcode submit region end(Prohibit modification and deletion)
